// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/tools/quic/quic_in_memory_cache.h"

#include <utility>

#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "net/quic/quic_bug_tracker.h"
#include "net/spdy/spdy_http_utils.h"

using base::FilePath;
using base::IntToString;
using base::StringPiece;
using std::list;
using std::string;

namespace net {

namespace {

    class ResourceFileImpl : public net::QuicInMemoryCache::ResourceFile {
    public:
        explicit ResourceFileImpl(const base::FilePath& file_name)
            : ResourceFile(file_name)
        {
        }

        void Read() override
        {
            base::ReadFileToString(FilePath(file_name_), &file_contents_);

            int file_len = static_cast<int>(file_contents_.length());
            int headers_end = HttpUtil::LocateEndOfHeaders(file_contents_.data(), file_len);
            if (headers_end < 1) {
                LOG(DFATAL) << "Headers invalid or empty, ignoring: "
                            << file_name_.value();
                return;
            }
            http_headers_ = new HttpResponseHeaders(
                HttpUtil::AssembleRawHeaders(file_contents_.data(), headers_end));

            if (http_headers_->GetNormalizedHeader("X-Original-Url", &url_)) {
                x_original_url_ = StringPiece(url_);
                HandleXOriginalUrl();
            }

            // X-Push-URL header is a relatively quick way to support sever push
            // in the toy server.  A production server should use link=preload
            // stuff as described in https://w3c.github.io/preload/.
            StringPiece x_push_url("X-Push-Url");
            if (http_headers_->HasHeader(x_push_url)) {
                size_t iter = 0;
                std::unique_ptr<string> push_url(new string());
                while (
                    http_headers_->EnumerateHeader(&iter, x_push_url, push_url.get())) {
                    push_urls_.push_back(StringPiece(*push_url));
                    push_url_values_.push_back(std::move(push_url));
                    push_url.reset(new string());
                }
            }

            body_ = StringPiece(file_contents_.data() + headers_end,
                file_contents_.size() - headers_end);

            CreateSpdyHeadersFromHttpResponse(*http_headers_, HTTP2, &spdy_headers_);
        }

    private:
        scoped_refptr<HttpResponseHeaders> http_headers_;
        string url_;
        list<std::unique_ptr<string>> push_url_values_;

        DISALLOW_COPY_AND_ASSIGN(ResourceFileImpl);
    };

} // namespace

QuicInMemoryCache::ServerPushInfo::ServerPushInfo(GURL request_url,
    SpdyHeaderBlock headers,
    net::SpdyPriority priority,
    string body)
    : request_url(request_url)
    , headers(std::move(headers))
    , priority(priority)
    , body(body)
{
}

QuicInMemoryCache::ServerPushInfo::ServerPushInfo(const ServerPushInfo& other)
    : request_url(other.request_url)
    , headers(other.headers.Clone())
    , priority(other.priority)
    , body(other.body)
{
}

QuicInMemoryCache::Response::Response()
    : response_type_(REGULAR_RESPONSE)
{
}

QuicInMemoryCache::Response::~Response() { }

QuicInMemoryCache::ResourceFile::ResourceFile(const base::FilePath& file_name)
    : file_name_(file_name)
    , file_name_string_(file_name.AsUTF8Unsafe())
{
}

QuicInMemoryCache::ResourceFile::~ResourceFile() { }

void QuicInMemoryCache::ResourceFile::SetHostPathFromBase(StringPiece base)
{
    size_t path_start = base.find_first_of('/');
    DCHECK_LT(0UL, path_start);
    host_ = base.substr(0, path_start);
    size_t query_start = base.find_first_of(',');
    if (query_start > 0) {
        path_ = base.substr(path_start, query_start - 1);
    } else {
        path_ = base.substr(path_start);
    }
}

StringPiece QuicInMemoryCache::ResourceFile::RemoveScheme(StringPiece url)
{
    if (url.starts_with("https://")) {
        url.remove_prefix(8);
    } else if (url.starts_with("http://")) {
        url.remove_prefix(7);
    }
    return url;
}

void QuicInMemoryCache::ResourceFile::HandleXOriginalUrl()
{
    StringPiece url(x_original_url_);
    // Remove the protocol so we can add it below.
    url = RemoveScheme(url);
    SetHostPathFromBase(url);
}

// static
QuicInMemoryCache* QuicInMemoryCache::GetInstance()
{
    return base::Singleton<QuicInMemoryCache>::get();
}

const QuicInMemoryCache::Response* QuicInMemoryCache::GetResponse(
    StringPiece host,
    StringPiece path) const
{
    ResponseMap::const_iterator it = responses_.find(GetKey(host, path));
    if (it == responses_.end()) {
        DVLOG(1) << "Get response for resource failed: host " << host << " path "
                 << path;
        if (default_response_.get()) {
            return default_response_.get();
        }
        return nullptr;
    }
    return it->second;
}

typedef QuicInMemoryCache::ServerPushInfo ServerPushInfo;

void QuicInMemoryCache::AddSimpleResponse(StringPiece host,
    StringPiece path,
    int response_code,
    StringPiece body)
{
    SpdyHeaderBlock response_headers;
    response_headers[":status"] = IntToString(response_code);
    response_headers["content-length"] = IntToString(static_cast<int>(body.length()));
    AddResponse(host, path, std::move(response_headers), body);
}

void QuicInMemoryCache::AddSimpleResponseWithServerPushResources(
    StringPiece host,
    StringPiece path,
    int response_code,
    StringPiece body,
    list<ServerPushInfo> push_resources)
{
    AddSimpleResponse(host, path, response_code, body);
    MaybeAddServerPushResources(host, path, push_resources);
}

void QuicInMemoryCache::AddDefaultResponse(Response* response)
{
    default_response_.reset(response);
}

void QuicInMemoryCache::AddResponse(StringPiece host,
    StringPiece path,
    SpdyHeaderBlock response_headers,
    StringPiece response_body)
{
    AddResponseImpl(host, path, REGULAR_RESPONSE, std::move(response_headers),
        response_body, SpdyHeaderBlock());
}

void QuicInMemoryCache::AddResponse(StringPiece host,
    StringPiece path,
    SpdyHeaderBlock response_headers,
    StringPiece response_body,
    SpdyHeaderBlock response_trailers)
{
    AddResponseImpl(host, path, REGULAR_RESPONSE, std::move(response_headers),
        response_body, std::move(response_trailers));
}

void QuicInMemoryCache::AddSpecialResponse(StringPiece host,
    StringPiece path,
    SpecialResponseType response_type)
{
    AddResponseImpl(host, path, response_type, SpdyHeaderBlock(), "",
        SpdyHeaderBlock());
}

QuicInMemoryCache::QuicInMemoryCache() { }

void QuicInMemoryCache::ResetForTests()
{
    STLDeleteValues(&responses_);
    server_push_resources_.clear();
}

void QuicInMemoryCache::InitializeFromDirectory(const string& cache_directory)
{
    if (cache_directory.empty()) {
        QUIC_BUG << "cache_directory must not be empty.";
        return;
    }
    VLOG(1) << "Attempting to initialize QuicInMemoryCache from directory: "
            << cache_directory;
    FilePath directory(FilePath::FromUTF8Unsafe(cache_directory));
    base::FileEnumerator file_list(directory, true, base::FileEnumerator::FILES);
    list<std::unique_ptr<ResourceFile>> resource_files;
    for (FilePath file_iter = file_list.Next(); !file_iter.empty();
         file_iter = file_list.Next()) {
        // Need to skip files in .svn directories
        if (file_iter.value().find(FILE_PATH_LITERAL("/.svn/")) != string::npos) {
            continue;
        }

        std::unique_ptr<ResourceFile> resource_file(
            new ResourceFileImpl(file_iter));

        // Tease apart filename into host and path.
        StringPiece base(resource_file->file_name());
        base.remove_prefix(cache_directory.length());
        if (base[0] == '/') {
            base.remove_prefix(1);
        }

        resource_file->SetHostPathFromBase(base);
        resource_file->Read();

        AddResponse(resource_file->host(), resource_file->path(),
            resource_file->spdy_headers().Clone(), resource_file->body());

        resource_files.push_back(std::move(resource_file));
    }

    for (const auto& resource_file : resource_files) {
        list<ServerPushInfo> push_resources;
        for (const auto& push_url : resource_file->push_urls()) {
            GURL url(push_url);
            const Response* response = GetResponse(url.host(), url.path());
            if (!response) {
                QUIC_BUG << "Push URL '" << push_url << "' not found.";
                return;
            }
            push_resources.push_back(ServerPushInfo(url, response->headers().Clone(),
                net::kV3LowestPriority,
                response->body().as_string()));
        }
        MaybeAddServerPushResources(resource_file->host(), resource_file->path(),
            push_resources);
    }
}

list<ServerPushInfo> QuicInMemoryCache::GetServerPushResources(
    string request_url)
{
    list<ServerPushInfo> resources;
    auto resource_range = server_push_resources_.equal_range(request_url);
    for (auto it = resource_range.first; it != resource_range.second; ++it) {
        resources.push_back(it->second);
    }
    DVLOG(1) << "Found " << resources.size() << " push resources for "
             << request_url;
    return resources;
}

QuicInMemoryCache::~QuicInMemoryCache()
{
    STLDeleteValues(&responses_);
}

void QuicInMemoryCache::AddResponseImpl(StringPiece host,
    StringPiece path,
    SpecialResponseType response_type,
    SpdyHeaderBlock response_headers,
    StringPiece response_body,
    SpdyHeaderBlock response_trailers)
{
    DCHECK(!host.empty()) << "Host must be populated, e.g. \"www.google.com\"";
    string key = GetKey(host, path);
    if (ContainsKey(responses_, key)) {
        QUIC_BUG << "Response for '" << key << "' already exists!";
        return;
    }
    Response* new_response = new Response();
    new_response->set_response_type(response_type);
    new_response->set_headers(std::move(response_headers));
    new_response->set_body(response_body);
    new_response->set_trailers(std::move(response_trailers));
    DVLOG(1) << "Add response with key " << key;
    responses_[key] = new_response;
}

string QuicInMemoryCache::GetKey(StringPiece host, StringPiece path) const
{
    return host.as_string() + path.as_string();
}

void QuicInMemoryCache::MaybeAddServerPushResources(
    StringPiece request_host,
    StringPiece request_path,
    list<ServerPushInfo> push_resources)
{
    string request_url = GetKey(request_host, request_path);

    for (const auto& push_resource : push_resources) {
        if (PushResourceExistsInCache(request_url, push_resource)) {
            continue;
        }

        DVLOG(1) << "Add request-resource association: request url " << request_url
                 << " push url " << push_resource.request_url
                 << " response headers " << push_resource.headers.DebugString();
        server_push_resources_.insert(std::make_pair(request_url, push_resource));
        string host = push_resource.request_url.host();
        if (host.empty()) {
            host = request_host.as_string();
        }
        string path = push_resource.request_url.path();
        if (responses_.find(GetKey(host, path)) == responses_.end()) {
            // Add a server push response to responses map, if it is not in the map.
            StringPiece body = push_resource.body;
            DVLOG(1) << "Add response for push resource: host " << host << " path "
                     << path;
            AddResponse(host, path, push_resource.headers.Clone(), body);
        }
    }
}

bool QuicInMemoryCache::PushResourceExistsInCache(string original_request_url,
    ServerPushInfo resource)
{
    auto resource_range = server_push_resources_.equal_range(original_request_url);
    for (auto it = resource_range.first; it != resource_range.second; ++it) {
        ServerPushInfo push_resource = it->second;
        if (push_resource.request_url.spec() == resource.request_url.spec()) {
            return true;
        }
    }
    return false;
}

} // namespace net
